package com.katesoft.scale4j.persistent.client;

import static com.katesoft.scale4j.common.utils.AssertUtility.assertNotNull;
import static org.hibernate.envers.AuditReaderFactory.get;

import java.sql.SQLException;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import net.jcip.annotations.ThreadSafe;

import org.apache.commons.lang.StringUtils;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.queryParser.MultiFieldQueryParser;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.util.Version;
import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.DefaultRevisionEntity;
import org.hibernate.envers.RevisionType;
import org.hibernate.envers.query.AuditEntity;
import org.hibernate.envers.query.AuditQuery;
import org.hibernate.search.FullTextSession;
import org.hibernate.search.Search;
import org.perf4j.aop.Profiled;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.orm.hibernate3.HibernateCallback;

import com.katesoft.scale4j.common.services.IBeanNameReferences;
import com.katesoft.scale4j.log.LogFactory;
import com.katesoft.scale4j.log.Logger;
import com.katesoft.scale4j.persistent.enums.CrudType;
import com.katesoft.scale4j.persistent.model.RevisionDomainEntity;
import com.katesoft.scale4j.persistent.model.unified.AbstractPersistentEntity;
import com.katesoft.scale4j.persistent.types.RevisionData;

/**
 * Default implementation of IHibernateDaoSupport interface.
 * <p/>
 * jvmcluster-persistent provides audit capability and can track history of domain entity (hibernate
 * envers module is used for this purpose, so there are no specific requirements for target client
 * to be able to use historical management).
 * <p/>
 * Also entity indexing capability included as well(hibernate search is used for this).
 * <p/>
 * This class contains few method for historical browsing.
 * <p/>
 * Method of this class marked with Profiled annotation, so runtime statistic can be gathered(users
 * would need to define profiled aspect or use jvmcluster-rttp module).
 * 
 * @author kate2007
 */
@ThreadSafe
public class LocalHibernateDaoSupport implements IHibernateDaoSupport {
   private final Logger logger = LogFactory.getLogger(getClass());
   private LocalHibernateTemplate hibernateTemplate;

   @Override
   @Profiled(
             tag = "dao.support.search_{$1.name}",
             message = "search called with parameters[queryString={$0} class={$1.name} properties={$2}]")
   public <T extends AbstractPersistentEntity> List<T> search(final String queryString,
            final Class<T> entityClass, final String[] properties) {
      return hibernateTemplate.execute(new HibernateCallback<List<T>>() {
         @SuppressWarnings("unchecked")
         @Override
         public List<T> doInHibernate(final Session session) throws HibernateException {
            if (StringUtils.isBlank(queryString)) {
               final Criteria criteria = session.createCriteria(entityClass);
               hibernateTemplate.prepareCriteria(criteria);
               return criteria.list();
            }
            String q = queryString.trim();
            if (!q.endsWith("*")) {
               q += "*";
            }
            FullTextSession fullTextSession = Search.getFullTextSession(session);
            QueryParser parser = new MultiFieldQueryParser(Version.LUCENE_30, properties,
                     new StandardAnalyzer(Version.LUCENE_30));
            parser.setAllowLeadingWildcard(true);
            parser.setDefaultOperator(QueryParser.Operator.AND);
            org.apache.lucene.search.Query luceneQuery;
            try {
               luceneQuery = parser.parse(q);
               org.hibernate.Query query = fullTextSession.createFullTextQuery(luceneQuery,
                        entityClass);
               hibernateTemplate.prepareQuery(query);
               return query.list();
            } catch (ParseException e) {
               throw new HibernateException(e);
            }
         }
      });
   }

   @SuppressWarnings({ "unchecked" })
   @Profiled(
             tag = "dao.support.loadForRevision_{$0.name}",
             message = "loadForRevision called with parameters [class={$0.name} uid={$1} revision={$2}]")
   protected <T extends AbstractPersistentEntity> T loadForRevision(final Class<T> entityClass,
            final long uniqueIdentifier, final Number revisionNumber) {
      return (T) hibernateTemplate.execute(new HibernateCallback<Object>() {
         @Override
         public Object doInHibernate(Session session) throws HibernateException, SQLException {
            T result = get(session).find(entityClass, uniqueIdentifier, revisionNumber);
            if (result != null) {
               result.forceAttributesLoad();
               logger.debug("entity %s.%s loaded for revision = %s", entityClass.getSimpleName(),
                        uniqueIdentifier, revisionNumber);
            }
            return result;
         }
      });
   }

   @Override
   @Profiled(
             tag = "dao.support.latestRevision_{$0.name}",
             message = "latestRevision called with parameters[class={$0.name} uid={$1}]")
   public <T extends AbstractPersistentEntity> Number latestRevision(final Class<T> entityClass,
            final long uniqueIdentifier) {
      return hibernateTemplate.execute(new HibernateCallback<Number>() {
         @Override
         public Number doInHibernate(Session session) throws HibernateException, SQLException {
            try {
               Number number = (Number) get(session).createQuery()
                        .forRevisionsOfEntity(entityClass, true, true)
                        .addProjection(AuditEntity.revisionNumber().max())
                        .add(AuditEntity.id().eq(uniqueIdentifier)).getSingleResult();
               if (number != null) {
                  logger.debug("max_rev = %s for entity_class %s.%s", number,
                           entityClass.getSimpleName(), uniqueIdentifier);
               }
               return number;
            } catch (javax.persistence.NoResultException e) {
               logger.debug(
                        "unable to find revision number by[entity_class=%s,unique_identifier=%s]",
                        entityClass.getName(), uniqueIdentifier);
            }
            return null;
         }
      });
   }

   @Override
   @Profiled(
             tag = "dao.support.allRevisions_{$0.name}",
             message = "allRevisions called with parameters[class={$0.name} uid={$1}]")
   public <T extends AbstractPersistentEntity> List<Number> allRevisions(
            final Class<T> entityClass, final long uniqueIdentifier) {
      return hibernateTemplate.execute(new HibernateCallback<List<Number>>() {
         @SuppressWarnings({ "unchecked" })
         @Override
         public List<Number> doInHibernate(Session session) throws HibernateException, SQLException {
            List<Number> l = get(session).createQuery()
                     .forRevisionsOfEntity(entityClass, true, true)
                     .add(AuditEntity.id().eq(uniqueIdentifier))
                     .addProjection(AuditEntity.revisionNumber())
                     .addOrder(AuditEntity.revisionNumber().desc()).getResultList();
            if (l != null) {
               logger.debug("revisions %s for entity_class %s.%s", l, entityClass.getSimpleName(),
                        uniqueIdentifier);
            }
            return l;
         }
      });
   }

   @Override
   @Profiled(
             tag = "dao.support.loadAllRevisions_{$0.name}",
             message = "loadAllRevisions called with parameters[class={$0.name} uid={$1}]")
   public <T extends AbstractPersistentEntity> List<RevisionData<T>> loadAllRevisions(
            final Class<T> entityClass, final long uniqueIdentifier) {
      return hibernateTemplate.execute(new HibernateCallback<List<RevisionData<T>>>() {
         @SuppressWarnings({ "unchecked", "rawtypes" })
         @Override
         public List<RevisionData<T>> doInHibernate(Session session) throws HibernateException,
                  SQLException {
            List<RevisionData<T>> result = new LinkedList<RevisionData<T>>();
            List list = get(session).createQuery().forRevisionsOfEntity(entityClass, false, true)
                     .add(AuditEntity.id().eq(uniqueIdentifier))
                     .addOrder(AuditEntity.revisionNumber().desc()).getResultList();
            for (Object o : list) {
               Object[] revisionsData = (Object[]) o;
               T entity = (T) revisionsData[0];
               entity.forceAttributesLoad();
               result.add(new RevisionData<T>(entity, revisionsData[1], CrudType
                        .from((RevisionType) revisionsData[2])));
            }
            return result;
         }
      });
   }

   @Override
   @Profiled(
             tag = "dao.support.latestRevisionForDate_{$0.name}",
             message = "latestRevisionForDate called with parameters[class={$0.name} date={$1} uid={$2}]")
   public <T extends AbstractPersistentEntity> Map<Long, RevisionData<T>> latestRevisionForDate(
            final Class<T> entityClass, final Date date, final Collection<Long> uniqueIdentifiers,
            final String prop) {
      assertNotNull(date, "revision date can't be null");
      return hibernateTemplate.execute(new HibernateCallback<Map<Long, RevisionData<T>>>() {
         @SuppressWarnings({ "unchecked", "rawtypes" })
         @Override
         public Map<Long, RevisionData<T>> doInHibernate(Session session)
                  throws HibernateException, SQLException {
            AuditReader auditReader = get(session);
            Map<Long, RevisionData<T>> result = new LinkedHashMap<Long, RevisionData<T>>(
                     uniqueIdentifiers.size());
            for (Long id : uniqueIdentifiers) {
               try {
                  Object obj = null;
                  AuditQuery query = auditReader.createQuery()
                           .forRevisionsOfEntity(entityClass, false, true)
                           .add(AuditEntity.id().eq(id));
                  if (prop != null) {
                     obj = query.add(
                              AuditEntity.property(prop).maximize()
                                       .add(AuditEntity.property(prop).le(date))).getSingleResult();
                  } else {
                     query.addOrder(AuditEntity.revisionNumber().desc());
                     List resultList = query.getResultList();
                     for (Object o : resultList) {
                        Object[] singleResult = (Object[]) o;
                        Object revisionEntity = singleResult[1];
                        long timestamp;
                        if (revisionEntity instanceof DefaultRevisionEntity) {
                           timestamp = ((DefaultRevisionEntity) revisionEntity).getTimestamp();
                        } else {
                           timestamp = ((RevisionDomainEntity) revisionEntity).getTimestamp();
                        }
                        if (timestamp <= date.getTime()) {
                           obj = o;
                           break;
                        }
                     }
                  }
                  if (obj != null) {
                     Object[] singleResult = (Object[]) obj;
                     T entity = (T) singleResult[0];
                     entity.forceAttributesLoad();
                     result.put(
                              id,
                              new RevisionData<T>(entity, singleResult[1], CrudType
                                       .from((RevisionType) singleResult[2])));
                  }
               } catch (javax.persistence.NoResultException e) {
                  logger.debug(
                           "unable to find revision data by[entity_class=%s,date=%s,unique_identifier=%s]",
                           entityClass.getName(), date, id);
               }
            }
            return result;
         }
      });
   }

   @Override
   public <T extends AbstractPersistentEntity> RevisionData<T> latestRevisionForDate(
            Class<T> entityClass, Date date, long uniqueIdentifier, String prop) {
      Map<Long, RevisionData<T>> result = latestRevisionForDate(entityClass, date,
               Collections.singleton(uniqueIdentifier), prop);
      if (!result.isEmpty()) {
         return result.values().iterator().next();
      }
      return null;
   }

   @Required
   @Autowired
   @Qualifier(IBeanNameReferences.HIBERNATE_TEMPLATE)
   public void setHibernateTemplate(LocalHibernateTemplate hibernateTemplate) {
      this.hibernateTemplate = hibernateTemplate;
   }

   @Override
   public IHibernateOperations getLocalHibernateTemplate() {
      return hibernateTemplate;
   }
}
